#####################################################################
# File:         KERLProcessor.py
# Product:      KnowledgeEngine
#
# Copyright:    Copyright (c) 2002-2004, Faulkner Technologies.
# Author:       Brian Oliver
#
# Description:  This file defines functions for evaluating and
#               translating KERL expressions, including evaluating
#               KERL expressions "on the server-side" and translating
#               KERL expressions into JavaScript, for implementing
#               showif.
#
#               Included in this file are functions for manipulating
#               and optimizing the representation of KERL expressions.
#
# See Also:     Component, KERLExtensions
#####################################################################

#####################################################################
## KnowledgeEngine Library Imports
from Common import *

#####################################################################
## Zope Library Imports
# (none)

 
#####################################################################
## Python Library Imports
import types

#####################################################################

def parseKERL(KERLstring):
    """
        Parses and returns an internal representation of the KERL string        
    """

    if KERLstring == "":
        return None
    else:
        return eval(KERLstring)
    
#####################################################################

def findComponent(identity, model, componentContext):
    """
        Attempts to locate and return the (component, componentContext)
        for the component with the specified identity
    """

    #is the component in the specified model?
    if model.containsComponent(identity):

        #get the component
        component = model.getComponent(identity)

        #is the component in a library?
        if component.isInLibrary():

            #component in library... we need to find it's context
            #based on it's identity (libraryitem mangled)

            #get the library item identity itself (by splitting the string at the content separator _)
            pos = identity.find("_")
            if pos >= 0:
                
                #get the library item identity
                libraryItemIdentity = identity[:pos]

                #get the library item component
                (libraryItem, dumbyComponentContext) = findComponent(libraryItemIdentity, model, componentContext)
                
                #get the component using the identity with the library item removed
                return findComponent(identity[pos+1:], model, libraryItem.getComponentContext())

            else:
                #certainly unknown!
                ###print "DEBUG: KERLProcessor.findComponent(%s): Component Not Found In LibraryItem" % (identity,)
                
                return (None, None)

        else:
            #normal component in model
            return (component, None)

    else:
        #do we have a component context?
        if componentContext != None:

            #get the library from the context
            library = componentContext.getLibrary()
            
            #is the component located in the library?
            if library.containsComponent(identity):
                return (library.getComponent(identity), componentContext)

            else:
                #certainly unknown!
                ###print "DEBUG: KERLProcessor.findComponent(%s): Component Not Found In Context" % (identity,)
                
                return (None, None)
        else:
            ##attempt to find the component assuming the identity contains the library item identity (contextual information)
            
            #get the library item identity itself (by splitting the string at the content separator _)
            pos = identity.find("_")
            if pos >= 0:
                
                #get the library item identity
                libraryItemIdentity = identity[:pos]

                #get the library item component
                (libraryItem, dumbyComponentContext) = findComponent(libraryItemIdentity, model, componentContext)
                
                #get the component using the identity with the library item removed
                return findComponent(identity[pos+1:], model, libraryItem.getComponentContext())

            else:
                #certainly unknown!
                ###print "DEBUG: KERLProcessor.findComponent(%s): Component Not Found In LibraryItem" % (identity,)
                
                return (None, None)
    
#####################################################################

def evaluateScoreOperator(expression, assessmentCache, componentContext = None):
    """
        Evaluates 'score' operator expression
        with the given cache and component context
        
        PRECONDITION:  The component for which we are calculating the score
        is shown (we assume this!).   This should be a safe assumption as
        the transitive closure for a score operator includes the 
        showif for the said component.
    """
    
    #is the component the model?
    if expression[1] == 'model':    
        component = assessmentCache.getModel()

    else:
        #get the component
        componentIdentity = expression[1]
        component = assessmentCache.getModel().getComponent(componentIdentity)
    
    #does the component exist and does it implement the readable score interface?
    if component != None and component.isInstanceOf("ReadableScore"):
    
        #get the score
        score = component.getScore(assessmentCache, componentContext)

        #get the operator
        operator = expression[2]
        
        #get the value (and ensure it is a floating point)       
        value = float(expression[3])
        
        #now test the value
        if operator in ['=', '==', 'equal', 'equals']:
            return score == value
        elif operator in ['!=', 'notequal', 'notequals']:
            return score != value
        elif operator in ['>', 'greaterthan']:
            return score > value
        elif operator in ['>=', '=>', 'greaterthanorequaltoo']:
            return score >= value
        elif operator in ['<', 'lessthan']:
            return score > value
        elif operator in ['<=', '=<', 'lessthanorequaltoo']:
            return score <= value
        else:
            return false
    else:
        #if the component is not available or doesn't implement the readable score interface, this operator should fail
        return false
            
#####################################################################

def toJavaScript(expression, model, componentContext = None):
    """
        Returns a Python string that represents a JavaScript
        expression, which may be used to evaluate the status
        of the KERL expression (in a web browser)

        NOTE: this method is primarily used to produce
        JavaScript expressions that form the basis of the
        client-side showif technology.

        PARAM: expression - a KERL expression in parsed format (see parseKERL)
        PARAM: model - a Model instance to scope the evaluation of the expression

        RETURNS: "" if there is no expression

        RETURN TYPE: String         
    """

    #is there an expression to evaluate?
    if expression == 'always':
        return 'true'
        
    elif expression == 'never':
        return 'false'
        
    elif expression != None:
        
        #get the base operator
        opr = expression[0]

        #is the operator a built in KERL operator? (all, any, none)
        if opr == "all":
            bopr = " & "
            iopr = ""
            popr = ""

        elif opr == "any":
            bopr = " | "
            iopr = ""
            popr = ""

        elif opr == "none":
            bopr = " & "
            iopr = "!("
            popr = ")"
        
        elif opr == "score":
        
            #TODO: one day we should make the 'score' operator work on the client-side!
            return ""

        else:
            #assume it is a component specific operator
            #(opr holds the component identity)
            (component, componentContext) = findComponent(opr, model, componentContext)
            
            #have the component return the javascript for the expression
            return component.toJavaScript(expression, componentContext)

        #build the javascript expression (recursively)
        result = ""

        #enumerate over each of the sub expressions, converting them to javascript
        #as necessary
        for subexpression in expression[1:]:
            #add the binary operator if necessary
            if result != "":
                result = result + bopr
            
            #is the operator of the subexpression a KERL built one?
            if subexpression[0] == "all" or subexpression[0] == "any" or subexpression[0] == "none":
                result = result + iopr + "(" + toJavaScript(subexpression, model, componentContext) + ")" + popr
            else:
                #the expression is a component extension.. so get the component
                (component, thisComponentContext) = findComponent(subexpression[0], model, componentContext)             

                #have the component return the javascript for the expression
                result = result + iopr + component.toJavaScript(subexpression, thisComponentContext) + popr

        return result
    
    else:
        return ""

#####################################################################

def resolveDynamicReferences(expression, component, assessmentCache):
    """
        Resolve 'reference' clauses in the specified expression
        for the specified component in the provided assessmentCache
    """
    
    if expression is None or len(expression) == 0:
        return None
    else:
        #process each element of the expression, resolving the dynamic references as necessary
        result = []
        
        for element in expression:

            #is the element a dynamic reference?            
            if type(element) is types.ListType and element[0] == "reference":

                #determine the kind of reference
                reference = element[1]
                
                if reference == "previous":
                    #get the type of component we want to locate
                    instanceType = element[2]

                    #locate the desired component from the specified component
                    referencedComponent = component.getPreviousComponentByInstanceOf(instanceType, assessmentCache)

                    #did we locate it?                
                    if referencedComponent is not None:
                        #we found the referenced component so we must return it's identity
                        result = result + [referencedComponent.getIdentity()]
                    else:
                        #TODO: We couldn't find the required component... we should throw an exception
                        result = result + [element]

            #the element is some nested list            
            elif type(element) is types.ListType:
                result = result + [resolveDynamicReferences(element, component, assessmentCache)]
            
            #the element is atomic
            else:
                result = result + [element]
        
        return result
    
#####################################################################

def evaluate(expression, assessmentCache, componentContext = None):
    """
        Evaluate the specified expression using the specified assessment cache,
        (on the server-side) returning true or false if the expression holds.
    """

    #is there an expression to evaluate?
    if expression != None:
        
        #get the base operator
        opr = expression[0]

        #is the operator a built in KERL operator? (all, any, none)
        if opr == "all":
            bopr = " and "
            iopr = ""
            popr = ""

        elif opr == "any":
            bopr = " or "
            iopr = ""
            popr = ""

        elif opr == "none":
            bopr = " or "
            iopr = "!("
            popr = ")"

        elif opr == "score":
            
            #evaluate the score
            return evaluateScoreOperator(expression, assessmentCache, componentContext)

        else:
            #assume it is a component specific operator
            #(opr holds the component identity)
            (component, componentContext) = findComponent(opr, assessmentCache.getModel(), componentContext)
            
            #have the component return the evaluation of the expression
            return component.evaluate(expression, assessmentCache, componentContext)

        #build a python expression (recursively)
        result = ""

        #enumerate over each of the sub expressions, converting them to a python expression as necessary
        for subexpression in expression[1:]:
            #add the binary operator if necessary
            if result != "":
                result = result + bopr

            #is the operator of the subexpression a KERL built one?
            if subexpression[0] == "all" or subexpression[0] == "any" or subexpression[0] == "none":
                result = result + iopr + "(" + repr(evaluate(subexpression, assessmentCache, componentContext)) + ")" + popr
            else:
                #the expression is a component extension.. so get the component
                (component, thisComponentContext) = findComponent(subexpression[0], assessmentCache.getModel(), componentContext)

                #have the component return the javascript for the expression
                result = result + iopr + repr(component.evaluate(subexpression, assessmentCache, thisComponentContext)) + popr

        return eval(result)

    else:
        return true
    
#####################################################################

def getDependencies(expression, indexedContainer, componentContext = None):
    """
        Returns a dictionary of components that are required to
        evaluate the provided KERL expression with in the scope of
        the specified indexedContainer (model/library).
    """

    #is there an expression?
    if expression != None:
        
        #get the base operator
        opr = expression[0]

        #is the operator a built in KERL operator? (all, any, none)
        if opr in ["all", "any", "none"]:

            #start a resulting dictionary
            result = {}

            #enumerate over each of the sub expressions, determining their dependencies
            for subexpression in expression[1:]:

                #is the operator of the subexpression a KERL built one?
                if subexpression[0] in ["all", "any", "none"]:
                    #update the result dictionary with the entries from the subexpression
                    result.update(getDependencies(subexpression, indexedContainer, componentContext))
                    
                #is it an operator that doesn't have dependencies?
                elif subexpression[0] == "score":
                    
                    #nothing to do for these operators
                    pass
                    
                #is it a reference operator (that doesn't have dependencies)?
                elif type(subexpression[0]) is types.ListType and subexpression[0][0] == "reference":
                    
                    #nothing to do for this operator
                    pass

                else:
                    #the expression is a component extension.. so get the component
                    component = indexedContainer.getComponent(subexpression[0])

                    #was the component found? (if not, we have an invalid identity)
                    if component != None:
                    
                            #have the component return the javascript for the expression
                            result.update(component.getDependencies(expression, componentContext))

                    else:
                        #TODO: we should probably throw an exception here as the component identity is unknown!           
                        return result

            #return the result
            return result
        
        #the score operator
        elif opr == "score":
        
            #get the score component identity
            componentIdentity = expression[1]
            
            #is the component the model?
            if componentIdentity == 'model':
            
                #models have no external dependancies
                return {}
                
            else:
                #get the component
                component = indexedContainer.getComponent(componentIdentity)

                #was the component found? (if not, we have an invalid identity)
                if component != None:

                    #we are dependant only on the specified component                
                    return {component.getIdentity(componentContext):component}

                else:
                    #the component isn't an instance of the Component class
                    return {}

        #a dynamic reference operator
        elif type(opr) is types.ListType and opr[0] == "reference":
            #we have to return {} as the operator returns a dynamic reference (it is impossible to predict exact dependancies in such circumstances)
            return {}
            
        #a component operator
        else:
            #assume it is a component specific operator
            #(opr holds the component identity)
            component = indexedContainer.getComponent(opr)
            
            #was the component found? (if not, we have an invalid identity)
            if component != None:
            
                #have the component return the dependencies for the expression
                return component.getDependencies(expression, componentContext)
                
            else:
                #TODO: we should probably throw an exception here as the component identity is unknown!           
                return {}
        
    else:
        return {}
    
#####################################################################
def getComponentShowIfTransitiveClosure(component, model, assessmentCache = None, componentContext = None):
    """
        Returns a KERL expression representing the
        showif transitive closure for the specified component.
    """        

# A "transitive closure" is an expression containing all of the conditions, 
# both explict and implicit (via transitivity), that must be true for
# a component to be shown.
#
# The following defines the conditions underwhich a component is shown.
#
# A component is shown if and only if it's parents are shown and
# the showif expression for the component is either;
#     a). empty, or 
#     b). evaluates to true AND 
#         the components used within the showif expression
#         used to define the showif are also shown.  
#
#    Notice this definition is recursive as;
#     a). the parents must also be shown, and
#     b). each component in a showif expression must also be shown.
    
    #assume no result at first
    result = []

    #----------------------
    #RULE: A component is only shown iff it's parents are shown
    #and the showif for the said component permits the component
    #to be shown.
    #
    #
    
    #the parents of the component effect the component
    effectingComponents = component.getParents(componentContext)
    
    #add the component itself to the components possibily 
    #having an impact on the display of the component
    effectingComponents = [component] + effectingComponents
    
    #add conditions underwhich the effecting components are shown
    for effectingComponent in effectingComponents:
        #get the transitive closure for theeffectingComponentparent showif (with resolved dynamic references) and
        #add it to the result        
        expression = getRuleShowIfTransitiveClosure(resolveDynamicReferences(effectingComponent.getShowIfKERL(), effectingComponent, assessmentCache), model, assessmentCache, componentContext)
    
        #was a valid expression returned?
        if expression != None:
            result = result + [expression]
    
    #----------------------

    #add an "all" if there is more than one condition in the result
    if len(result) > 1:
        result = ['all'] + result
        
    elif len(result) == 1:
        #as there is only one result, use it without an operator
        result = result[0]

    elif len(result) == 0:
        result = None
        
    return result

#####################################################################

def getRuleShowIfTransitiveClosure(expression, model, assessmentCache = None, componentContext = None):
    """
        Returns a KERL expression representing the showif transitive
        closure for the specified expression.
    """

#params:  expression - a KERL expression in it's native python representation
#returns: a KERL expression in it's native python representation

# A "transitive closure" is an expression containing all of the conditions, 
# both explict and implicit (via transitivity), that must be true for
# a component to be shown.

# NOTE: this method is different from getComponentShowIfTransitiveClosure as it
#       expands the provided rule to create a transitive closure, where as
#       getComponentShowIfTransitiveClosure creates a transitive closure for
#       the specified component

    #do we have a KERL expression?
    if expression != None:

        #get the KERL operator
        opr = expression[0]

        #is the expression a KERL operator or a component extension
        if not opr in ['any', 'all', 'none', 'score']:
            #we're processing a component extension
            
            #get the component
            (component, componentContext) = findComponent(opr, model, componentContext)

            #determine the transitive closure for the component
            ctc = getComponentShowIfTransitiveClosure(component, model, assessmentCache, componentContext)

            #does the component have a transitive closure
            if ctc != None:                
               #the result is the transitive closure of the component AND the specified rule
                result = ['all', ctc, expression]                
            else:
                #no transitive closure, so the result is just the specified rule 
                result = expression

        elif opr == 'score':
                    
            #is the component the model?
            if expression[1] == 'model':
                #there is not transitive closure of a model (so the result is the expression itself)
                result = expression
                
            else:
                #get the component
                (component, componentContext) = findComponent(expression[1], model, componentContext)

                #determine the transitive closure for the component
                ctc = getComponentShowIfTransitiveClosure(component, model, assessmentCache, componentContext)

                #does the component have a transitive closure
                if ctc != None:                
                   #the result is the transitive closure of the component AND the specified rule
                    result = ['all', ctc, expression]                
                else:
                    #no transitive closure, so the result is just the specified rule 
                    result = expression

        else:
            #the expression uses a KERL built-in operator (containing an 'all', 'any' or 'none' operator)
            
            #get the operator (it will stay the same)
            result = [opr]

            #find the transitive closure for each of the sub expressions in the expression
            for subexpression in expression[1:]:
                
                #get the transitive closure for the current subexpression
                rtc = getRuleShowIfTransitiveClosure(subexpression, model, assessmentCache, componentContext)

                #was there a valid transitive closure?
                if rtc != None:
                    result = result + [rtc]              

        #any result? if not return None           
        if result == []:
            return None
        else:
            return result

    else:
        #if no rule is provided, then there is no transitive closure
        return None

#####################################################################

def updateKERLIdentities(expression, identityMappings, indexedContainer):
    """
        Updates component identity references (strings) in a KERL
        expression with those specified in the identityMappings
        dictionary (old -> new identities).
        
        Returns a new KERL containing updated identities.
    """
    
    #is there an expression?
    if expression != None:
        
        #get the base operator
        opr = expression[0]

        #is the operator a built in KERL operator? (all, any, none)
        if opr in ["all", "any", "none"]:

            #the operator is the same for the result
            result = [opr]

            #enumerate over each of the sub expressions, and update their identities
            for subexpression in expression[1:]:

                #update the subexpression (recursive)
                result += [updateKERLIdentities(subexpression, identityMappings, indexedContainer)]

            #return the result
            return result
        
        elif opr == "score":
 
            #get the old component identity
            oldIdentity = expression[1]
 
            #does the component have a new identity?
            if identityMappings.has_key(oldIdentity):
                #get the new component identity
                componentIdentity = identityMappings[oldIdentity]
            else:
                #old component.. no new identity 
                #(we still have to resolve mappings as the component rule 
                # may refer to other components)
                componentIdentity = oldIdentity
       
            #build the new operator
            return ["score", componentIdentity, expression[2], expression[3]]

        #a dynamic reference operator
        elif type(opr) is types.ListType and opr[0] == "reference":
            
            #nothing to update for the dynamic reference operator
            return expression

        #a component operator        
        else:
            #does the component have a new identity?
            if identityMappings.has_key(opr):
                #get the new component identity
                componentIdentity = identityMappings[opr]
            else:
                #old component.. no new identity 
                #(we still have to resolve mappings as the component rule 
                # may refer to other components)
                componentIdentity = opr

            #get the component
            component = indexedContainer.getComponent(componentIdentity)

            #was the component found? (if not, we have an invalid identity)
            if componentIdentity != None:

                #let the component resolve it's own kerl extension
                return component.updateKERLIdentities(expression, identityMappings)

            else:
                #TODO: we should probably throw an exception here as the component identity is unknown!           
                return None
        
    else:
        return None    
    
#####################################################################

def isComponentOperator(operator):
    """
        Returns if the kerl expression is a component operator
    """
    
    return not operator in ['any', 'all', 'none', 'score']
    
#####################################################################

def optimizeKERLForPageWithAssessment(expression, assessment, page):
    """
        Return an optimized showif kerl for the specified assessment (cache) on
        the specified page.
        
        This method effectively does partial evaluation of kerls for
        components that are NOT on the specified page, by retrieving
        the state of the said components from the provided assessment cache.
        
        Given the assessment state, there are three possible outcomes.
        1). the expression will always evaluate to true (always shown)
        2). the expression will always evaluate to false (never shown)
        3). some server-side evaluation will still need to take place
        
        RETURNS: 
        'always' - if the kerl will always evaluate to true (always shown)
        'never' - if the kerl will always evaluate to false (not shown)
        a kerl - an optimized kerl otherwise
    """

    if expression is None or len(expression) == 0:
        result = 'always'

    else:
        #get the operator from the expression
        opr = expression[0]
        
        if isComponentOperator(opr):
            #locate the component using the assessment model
            component = assessment.getModel().getComponent(opr)
            
            if component.isDescendant(page):
                #TODO:    return component.optimizeShowIfForPageWithAssessment(assessment, page)
                result = expression
            else:
                if evaluate(expression, assessment):
                    result = 'always'
                else:
                    result = 'never'
                
        else:
            #optimize the subexpressions
            subexpressions = [optimizeKERLForPageWithAssessment(subexpr, assessment, page) for subexpr in expression[1:]]

            #optimize the 'any' operator
            if opr == 'any':
                if 'always' in subexpressions:
                    result = 'always'                           #at least one is true!

                else:                
                    #remove 'never' values as they have no effect
                    subexpressions = [subexpr for subexpr in subexpressions if subexpr != 'never']

                    if len(subexpressions) == 0:
                        result = 'never'                        #no 'always' values means this expression fails
                    elif len(subexpressions) == 1:
                        result = subexpressions[0]              #as there is only 1 operator, the 'any' isn't needed
                    else:
                        result = [opr] + subexpressions 
            
            #optimize the 'all' operator
            elif opr == 'all':
                if 'never' in subexpressions:
                    result = 'never'                            #there should be no false values!
               
                else:
                    #remove 'always' values as they have no effect
                    subexpressions = [subexpr for subexpr in subexpressions if subexpr != 'always']

                    if len(subexpressions) == 0:
                        result = 'always'                       #no true values means this expression is good
                    elif len(subexpressions) == 1:
                        result = subexpressions[0]              #as there is only 1 operator, the 'all' isn't needed
                    else:
                        result = [opr] + subexpressions 
            
            #optimize the 'none' operator
            elif opr == 'none':
                if 'always' in subexpressions:
                    result = 'never'                            #there should be no true values!

                else:                
                    #remove 'never' values as they have no effect
                    subexpressions = [subexpr for subexpr in subexpressions if subexpr != 'never']

                    if len(subexpressions) == 0:
                        result = 'always'                       #no false values means this expression is good
                    else:
                        result = [opr] + subexpressions         #for anything else we need the operator
            
            else:
                #unknown operator - so let's just return it as is
                result = expression

        return result

#####################################################################
